package net.andersand.games.yatzy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

/**
 * A group of dice that can be thrown and analyzed. One die, several dice. <p/>
 * <a href="http://en.wikipedia.org/wiki/Yatzy#Swedish_scoring_variation">
 * Swedish scoring rules are used</a>
 * 
 * @author Anders Sandberg Nordbø
 */
class Dice {

	private List<Die> dice = new ArrayList<Die>();

	/**
	 * Normal constructor
	 * 
	 * @param dices
	 */
	Dice() {
		for (int i = 0; i < Yatzy.NUMBER_OF_DICES; i++) {
			add(new Die());
		}
	}

	private void add(Die die) {
		dice.add(die);
	}

	void setDice(List<Die> list) {
		dice.addAll(list);
	}

	List<Die> getDice() {
		return dice;
	}

	Die getDie(int j) {
		return dice.get(j);
	}

	/**
	 * Rolls the dice
	 */
	void throwDice() {
		for (Die d : dice) {
			if (!d.isLocked()) {
				d.roll();
			}
		}
	}

	/**
	 * Analyses the set of dices for any Yatzy combination
	 * 
	 * @return string analysis
	 */
	String analyze() {
		List<String> result = new ArrayList<String>();
		if (!getYatzy().isEmpty()) {
			result.add("Yatzy");
		}
		if (!getFullHouse().isEmpty()) {
			result.add("Full house");
		}
		if (!getLargeStraight().isEmpty()) {
			result.add("Large straight");
		}
		if (!getSmallStraight().isEmpty()) {
			result.add("Small straight");
		}
		if (!getFourOfAKind().isEmpty()) {
			result.add("Four of a kind");
		}
		if (!getThreeOfAKind().isEmpty()) {
			result.add("Three of a kind");
		}
		if (!getTwoPairs().isEmpty()) {
			result.add("Two pairs");
		}
		if (!getPair().isEmpty()) {
			result.add("Pair");
		}
		StringBuilder analysis = new StringBuilder();
		for (String s : result) {
			analysis.append(s + ", ");
		}
		return analysis.length() < 2 
					? analysis.toString() 
					: analysis.substring(0, analysis.length() - 2);
	}

	/**
	 * Analyzes the dice against a type of Yatzy score.
	 * 
	 * @param type
	 * @return the value obtainable for the type with current dice values
	 */
	int analyze(ScoreType type) {
		switch (type) {
		case ONES:
		case TWOS:
		case THREES:
		case FOURS:
		case FIVES:
		case SIXES:
			return summarizeByPlainValue(type.ordinal() + 1);
		case PAIR:
			return summarizeDice(getPair());
		case TWOPAIRS:
			return summarizeDice(getTwoPairs());
		case THREEOFAKIND:
			return summarizeDice(getThreeOfAKind());
		case FOUROFAKIND:
			return summarizeDice(getFourOfAKind());
		case SMALLSTRAIGHT:
			return summarizeDice(getSmallStraight());
		case LARGESTRAIGHT:
			return summarizeDice(getLargeStraight());
		case FULLHOUSE:
			return summarizeDice(getFullHouse());
		case YATZY:
			return summarizeDice(getYatzy()) > 0 ? 50 : 0;
		case CHANCE:
			return summarizeDice(dice);
		default:
			return -1;
		}
	}

	/**
	 * @param dice
	 * @return the sum of the dice
	 */
	private static int summarizeDice(List<Die> dice) {
		int sum = 0;
		for (Die d : dice) {
			sum += d.getValue();
		}
		return sum;
	}

	/**
	 * Wraps one or more dice into a list
	 * 
	 * @param dice
	 * @return a list of the passed dice
	 */
	private static List<Die> createDiceList(Die... dice) {
		List<Die> diceList = new ArrayList<Die>();
		for (Die d : dice) {
			diceList.add(d);
		}
		return diceList;
	}

	/**
	 * Returns the sum of dice with given value
	 * 
	 * @param filterValue
	 * @return the sum
	 */
	private int summarizeByPlainValue(int filterValue) {
		int sum = 0;
		for (Die d : dice) {
			if (filterValue == d.getValue()) {
				sum += d.getValue();
			}
		}
		return sum;
	}

	/**
	 * 1, 2, 3, 4, 5
	 * 
	 * @return
	 */
	List<Die> getSmallStraight() {
		Dice copy = copy();
		Collections.sort(copy.getDice());
		Iterator<Die> it = copy.getDice().iterator();
		Die previous = it.next();
		if (previous.getValue() != 1) {
			return Collections.emptyList();
		}
		while (it.hasNext()) {
			Die current = it.next();
			if (current.getValue() != previous.getValue() + 1) {
				return Collections.emptyList();
			}
			previous = current;
		}
		return copy.getDice();
	}

	/**
	 * 2, 3, 4, 5, 6
	 * 
	 * @return
	 */
	List<Die> getLargeStraight() {
		Dice copy = copy();
		Collections.sort(copy.getDice());
		Iterator<Die> it = copy.getDice().iterator();
		Die previous = it.next();
		if (previous.getValue() != 2) {
			return Collections.emptyList();
		}
		while (it.hasNext()) {
			Die current = it.next();
			if (current.getValue() != previous.getValue() + 1) {
				return Collections.emptyList();
			}
			previous = current;
		}
		return copy.getDice();
	}

	/**
	 * Full House consists of Three of a kind + Pair.
	 * 
	 * <p>
	 * <h3>Implementation details:</h3>
	 * Identify various full house combinations.
	 * <ul>
	 * <li>6, 3, 6, 3, 3 (ordered: 33366)</li>
	 * <li>6, 3, 6, 6, 3 (ordered: 33666)</li>
	 * </ul>
	 * <p/> Need to look for three of a kind first, because pair is a subset of
	 * three of a kind.
	 * </p>
	 * TODO maybe some reuse of getXOfAKind(2) and getXOfAKind(3) is possible ?
	 * 
	 * @return
	 */
	List<Die> getFullHouse() {
		List<Die> ret = new ArrayList<Die>();
		Dice copy = copy();
		Collections.sort(copy.getDice());
		// Look for three of a kind and remove those dice from the list
		Iterator<Die> it = copy.getDice().iterator();
		Die previous1 = it.next();
		Die previous2 = it.next();
		int threeOfAKindValue = 0;
		while (it.hasNext()) {
			Die current = it.next();
			if (current.equals(previous1) && previous1.equals(previous2)) {
				threeOfAKindValue = current.getValue();
				copy.getDice().remove(current);
				copy.getDice().remove(previous1);
				copy.getDice().remove(previous2);
				ret.addAll(createDiceList(current, previous1, previous2));
				break;
			}
			previous2 = previous1;
			previous1 = current;
		}
		if (threeOfAKindValue == 0) {
			return Collections.emptyList();
		}
		it = copy.getDice().iterator();
		Die previous = it.next();
		int pairValue = 0;
		// Find a pair from the remaining dice.
		while (it.hasNext()) {
			Die current = it.next();
			if (current.equals(previous)) {
				pairValue = current.getValue();
				ret.addAll(createDiceList(current, previous));
				break;
			}
			previous = current;
		}
		if (pairValue == 0) {
			return Collections.emptyList();
		}
		// If three of a kind has a different value than the
		// pair, Full house is identified
		return pairValue != threeOfAKindValue ? ret : Collections
				.<Die> emptyList();
	}

	/**
	 * Yatzy is when all dice show the same value.
	 * 
	 * @return true if yatzy
	 */
	List<Die> getYatzy() {
		return getXOfAKind(5);
	}

	/**
	 * A pair is found if two dice have the same value
	 * TODO maybe just replace this with getXOfAKind(2) ?
	 * 
	 * @return a list of the dice identified
	 */
	List<Die> getPair() {
		Dice copy = copy();
		Collections.sort(copy.getDice());
		Iterator<Die> it = copy.getDice().iterator();
		Die previous = it.next();
		while (it.hasNext()) {
			Die d = it.next();
			if (d.equals(previous)) {
				return createDiceList(d, previous);
			}
			previous = d;
		}
		return Collections.emptyList();
	}

	/**
	 * Three of a kind is simply that.
	 * 
	 * @return true if three of a kind
	 */
	List<Die> getXOfAKind(int howMany) {
		Dice copy = copy();
		Collections.sort(copy.getDice());
		Iterator<Die> it = copy.getDice().iterator();
		List<Die>[] found = new ArrayList[Die.MAX_VALUE + 1];
		for (int i = 0; i < found.length; i++) {
			found[i] = new ArrayList<Die>();
		}
		while (it.hasNext()) {
			Die d = it.next();
			found[d.getValue()].add(d);
			if (found[d.getValue()].size() == howMany) {
				return found[d.getValue()];
			}
		}
		return Collections.emptyList();
	}

	private List<Die> getThreeOfAKind() {
		return getXOfAKind(3);
	}

	/**
	 * Four of a kind is just that.
	 * 
	 * @return true if four of a kind
	 */
	List<Die> getFourOfAKind() {
		return getXOfAKind(4);
	}

	/**
	 * Two pairs is when you have two distinct pairs. <p/> This is different
	 * from four of a kind, moreover two equal pairs should not be a subset of
	 * four of a kind.
	 * 
	 * @return a list of the dice identified
	 */
	List<Die> getTwoPairs() {
		// Add the dice that are part of the sequence to this list
		List<Die> ret = new ArrayList<Die>();

		// Make copies, don't mess with originals
		Dice copy = copy();
		Collections.sort(copy.getDice());

		// Find first pair
		Iterator<Die> it = copy.getDice().iterator();
		Die previous = it.next();
		int firstPairValue = 0;
		while (it.hasNext()) {
			Die d = it.next();
			if (d.equals(previous)) {
				firstPairValue = d.getValue();
				// Don't want to find the same pair again
				copy.getDice().remove(d);
				copy.getDice().remove(previous);
				// Add the pair to the return list
				ret.addAll(createDiceList(d, previous));
				break;
			}
			previous = d;
		}
		// If not one pair, then not two pairs.
		if (firstPairValue == 0) {
			return Collections.emptyList();
		}
		// reset helper variables
		it = copy.getDice().iterator();
		previous = it.next();
		int secondPairValue = 0;
		while (it.hasNext()) {
			Die d = it.next();
			if (d.equals(previous)) {
				secondPairValue = d.getValue();
				ret.addAll(createDiceList(d, previous));
				break;
			}
			previous = d;
		}
		// If two pairs found and the pairs are distinct in value,
		// two pairs are identified
		if (secondPairValue == 0 || firstPairValue == secondPairValue) {
			return Collections.emptyList();
		}
		return ret;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (Die d : dice) {
			sb.append(d.toString() + ", ");
		}
		String s = sb.toString();
		return s.substring(0, s.length() - 2);
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Dice)) {
			return false;
		}
		List<Die> other = ((Dice) obj).copy().dice;
		Collections.sort(other);
		List<Die> these = copy().dice;
		Collections.sort(these);
		for (int i = 0; i < these.size(); i++) {
			if (!these.get(i).equals(other.get(i))) {
				return false;
			}
		}
		return true;
	}

	class Die implements Comparable<Die> {
		public static final int MAX_VALUE = 6;
		private int value = -1;
		private boolean locked;

		Die() {

		}

		Die(int value) {
			this.value = value;
		}

		void roll() {
			value = (int) (6 * Math.random()) + 1;
		}

		public Die getCopy() {
			Die d = new Die();
			d.setValue(getValue());
			d.setLocked(isLocked());
			return d;
		}

		private void setValue(int value) {
			this.value = value;
		}

		int getValue() {
			return value;
		}

		boolean isLocked() {
			return locked;
		}

		void setLocked(boolean locked) {
			this.locked = locked;
		}

		void toggleLocked() {
			setLocked(!isLocked());
		}

		/**
		 * Compares dices to dices only
		 */
		public int compareTo(Die d) {
			return getValue() - d.getValue();
		}

		@Override
		public String toString() {
			return String.valueOf(getValue());
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof Die)) {
				return false;
			}
			return ((Die) obj).getValue() == getValue();
		}

		Die copy() {
			Die die = new Die();
			die.setValue(getValue());
			die.setLocked(isLocked());
			return die;
		}
	}

	Dice copy() {
		Dice d = new Dice();
		d.dice.clear();
		for (Die die : this.getDice()) {
			d.add(die.copy());
		}
		return d;
	}

	/**
	 * Prepares the dice for a new round
	 */
	public void reset() {
		for (Die d : dice) {
			d.setValue(0);
			d.setLocked(false);
		}
	}

}
